fix(claude): emit plan events for TodoWrite during input streaming#1541
Conversation
When Claude calls TodoWrite, emit turn.plan.updated events during input streaming so the plan sidebar displays Claude's todos the same way it already works for Codex plan steps. Events are emitted alongside existing tool lifecycle events, not as a replacement. Also passes through the data field on item.completed activities to match item.updated behavior, and auto-opens the plan sidebar when plan steps arrive. Closes pingdotgg#1539
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Plan state now falls back to the most recent plan from any previous turn when the current turn has no plan activity, so TodoWrite tasks stay visible across follow-up messages. Simplified redundant isTodoTool check.
There was a problem hiding this comment.
Pull request overview
This PR wires Claude’s TodoWrite tool into the existing plan sidebar by emitting turn.plan.updated events during Claude input streaming, and includes several related UX/data-flow fixes so todos and task activity render consistently in the UI.
Changes:
- Emit
turn.plan.updatedduring Claudeinput_json_deltaprocessing forTodoWrite, without suppressing existing tool lifecycle events. - Persist/restore plan state across turns and auto-open the plan sidebar when plan steps arrive.
- Improve work log rendering (include
task.completed, prefer task payload summaries) and forwarddataforitem.completedactivities.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/src/session-logic.ts | Persist active plan across turns; include task.completed; adjust task entry label/tone logic. |
| apps/web/src/session-logic.test.ts | Add coverage for plan fallback and updated work log filtering/label behavior. |
| apps/web/src/components/ChatView.tsx | Auto-open plan sidebar when an active plan appears (respecting dismiss state). |
| apps/server/src/provider/Layers/ClaudeAdapter.ts | Emit turn.plan.updated events for TodoWrite during streamed tool input parsing; improve tool request summaries for agent tools. |
| apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts | Forward payload.data for item.completed tool lifecycle activities. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Only force "thinking" tone for task.progress, not task.completed, so failed tasks preserve their error tone. Also check payload.detail for task labels since task.completed stores its summary there. Add regression test for failed task.completed rendering.
Use a sentinel string when turnId is null so the dismissed ref still gets set, preventing the auto-open effect from immediately reopening the sidebar.
Apply the same __dismissed__ sentinel to the onClose handler on the plan sidebar X button, matching the fix already applied to togglePlanSidebar.
Use the same turnKey fallback chain (activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__") in both the auto-open effect and the dismiss handlers so they always match.
Dynamically switch the sidebar label between "Plan" and "Tasks" based on context. When a proposed plan exists or the user is in plan mode, the label reads "Plan". Otherwise it reads "Tasks". Applies to the composer button, compact menu, sidebar badge, and aria labels.
…tail Only auto-open the sidebar for plans from the current turn, not fallbacks from previous turns. Add explicit parentheses to the label ternary for clarity. Skip detail assignment when the detail text is already used as the label to avoid duplication in the work log.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Quick summary of the iteration here since there are a few rounds of commits: Started by wiring TodoWrite into the existing plan event path so the sidebar actually shows Claude's tasks. Bugbot caught a few real issues (dismiss key mismatch, error tone override, label/detail duplication, auto-open on thread switch) each one got a focused fix. Midway through I thought this would need a dedicated task UI component separate from the plan sidebar, since the plan panel was designed around Codex's plan mode. Almost closed it to go rethink. Then realized the simpler answer: just dynamically label the sidebar "Tasks" vs "Plan" based on context. Same component, no new UI, no plan mode required. Users get task visibility without being forced into a workflow they didn't ask for. Current state: all Bugbot feedback resolved, tests passing, parentheses fix for the label ternary pushed. Ready for review. |
…into fix/claude-todowrite-plan-events
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Co-authored-by: codex <codex@users.noreply.github.com>
ApprovabilityVerdict: Needs human review Despite the 'fix' prefix, this PR introduces new feature capability: emitting plan events during TodoWrite streaming, auto-opening the plan sidebar, and changing how plans persist across turns. These are significant runtime behavior changes spanning server and client code that warrant human review. You can customize Macroscope's approvability policy. Learn more. |
Co-authored-by: codex <codex@users.noreply.github.com>

What Changed
Claude's
TodoWritetool calls now emitturn.plan.updatedevents during input streaming so the plan sidebar shows task progress in real-time. The plan event fires alongside the existing tool lifecycle events, not instead of them.Related fixes:
item.completedactivities now forward thedatafield to the UI, matchingitem.updatedtask.completedentries show up in the work log (previously filtered out withtask.started)payload.summarywhen available instead of the generic activity summaryCloses #1539
Why
TodoWritegets classified asfile_changeinclassifyToolItemTypebecause the name contains "write". It renders as a generic "File change - TodoWrite: {raw JSON}" line in the work log. Noturn.plan.updatedevent gets emitted, so the plan sidebar never activates for Claude sessions. This is core functionality that works for Codex but is completely broken for Claude.There's an existing PR for this (#1387) that intercepts at tool result time and replaces the normal
item.updated/content.deltaemissions. This PR takes a different approach:input_json_deltameans the sidebar populates as Claude writes the input, not after the full round-trip.deriveActivePlanStatefalls back to the most recent plan from any previous turn. Without this, the sidebar clears every time you send a follow-up.Validated with
bun fmt,bun lint,bun typecheck, andbun run test(all passing).UI Changes
Before -- TodoWrite is invisible to the sidebar
Tasks exist but are completely invisible. There is no way to see them.
After -- Tasks stream in live without plan mode
Tasks are now streamed in real-time and persist between turns.
After -- Button label adapts to context
When the session has tasks but no plan, the button shows "Tasks":
When the session is in plan mode, the same button shows "Plan":
Checklist
Note
Medium Risk
Touches provider event emission/ingestion and client-side plan/worklog derivation, so regressions could affect real-time activity rendering and sidebar behavior across turns.
Overview
Claude
input_json_deltahandling now detectsTodoWritetool calls, extracts todo steps, and emitsturn.plan.updatedevents during streaming (including a default "Task" label for blank items), while tool summaries for collab-agent invocations prefer human-readabledescription/promptover raw JSON.Runtime ingestion now forwards
payload.dataonitem.completedactivities, and the web app updates plan/task presentation: active plan state falls back to the latest plan from prior turns, the plan sidebar auto-opens when new steps arrive (respecting user dismissal), and the sidebar/button label switches between "Plan" and "Tasks" based on context.Work log derivation is adjusted to include
task.completed(still omittingtask.started), preferpayload.summary/payload.detailas task labels, and rendertask.progresswith a "thinking" tone.Reviewed by Cursor Bugbot for commit af3e93a. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Emit
turn.plan.updatedevents for TodoWrite tool input during Claude streaminginput_json_deltastreaming in ClaudeAdapter.ts, detects TodoWrite tools and emitsturn.plan.updatedruntime events with normalized plan steps (blankcontentbecomes"Task", status values mapped tocompleted/inProgress/pending).deriveActivePlanStatefalls back to the most recent plan from a prior turn when the current turn has none, andderiveWorkLogEntriesnow includestask.completedentries with labels sourced frompayload.summaryorpayload.detail.Macroscope summarized af3e93a.